package com.nf.dbutils;

import com.nf.dbutils.util.RsMetaUtils;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;

public class DefaultRowProcessor implements RowProcessor {
    public static final int PROPERTY_NOT_FOUND=-1;

//    利用spi技术来找到所有的propertyHandler接口的实现类
    private static final ServiceLoader<PropertyHandler> propertyHandlers=ServiceLoader.load(PropertyHandler.class);
    private final Map<String,String> propertyOverrides;

    public DefaultRowProcessor() {
        this(new HashMap<>());
    }

    public DefaultRowProcessor(Map<String, String> propertyOverrides) {
        super();
        if (propertyOverrides==null){
            throw new DaoExecutor("不能传一个空映射集合过来");
        }

        this.propertyOverrides = propertyOverrides;
    }

    public Object[] toArray(ResultSet rs) throws SQLException {

        ResultSetMetaData metaData=rs.getMetaData();
        int clos=metaData.getColumnCount();
        Object[] row=new Object[clos];

        for (int i = 0; i < clos; i++) {
            row[i]=rs.getObject(i+1);
        }
        
        return row;
    }

    public Map<String, Object> toMap(ResultSet rs) throws SQLException {
        Map<String,Object> map=new HashMap<>();

        ResultSetMetaData metaData=rs.getMetaData();
        int cols= metaData.getColumnCount();

        for (int i = 1; i <= cols; i++) {
            String columnName = RsMetaUtils.getColumnName(metaData, i);
            map.put(columnName,rs.getObject(i));
        }
        return map;
    }

    public <T> T toBean(ResultSet rs, Class<?> clz) throws SQLException {
        T bean=createInstance(clz);

        return this.populateBean(rs,bean);
    }

    /**
     * 此方法完成bean的属性的赋值
     * @param rs
     * @param bean
     * @param <T>
     * @return
     * @throws SQLException
     */
    public <T> T populateBean(ResultSet rs,T bean) throws SQLException {
//        获取实体类有多少属性
        PropertyDescriptor[] props=this.propertyDescriptors(bean.getClass());
//         解决结果集的列与属性匹配映射的功能
        ResultSetMetaData rsmd=rs.getMetaData();
        int[] columnToProperties = this.mapColumnsToProperties(rsmd, props);
//          真正的给bean的属性赋值的功能
        return populateBean(rs,bean,props,columnToProperties);
    }

    /**
     * 此方法会从数据库取数据,并且处理基本类型的问题
     * 要点:从数据库取值时是null值,并且字段是基本类型,那么就不能赋值为null
     * @param rs
     * @param bean
     * @param props
     * @param columnToProperties
     * @param <T>
     * @return
     * @throws SQLException
     */
    protected <T> T populateBean(ResultSet rs,T bean,PropertyDescriptor[] props,int[] columnToProperties) throws SQLException {
        for (int i = 0; i < columnToProperties.length; i++) {
//            没有映射关系不处理，直接往下走
            if (columnToProperties[i]==PROPERTY_NOT_FOUND){
                continue;
            }
//            有映射关系就处理
            PropertyDescriptor prop=props[columnToProperties[i]];

            Class<?> propertyType = prop.getPropertyType();
//            基础类型不可能为空，所以直接不处理
            if (propertyType==null){
                continue;
            }
//            走到下面就是propertyType不为空，并且有映射

//            直接用getObject从数据库取数据
//            不用原始代码的processColumn体系
//            因为jdbc的Mysql当前驱动版本，getObject也会去调用getXxx方法
            Object value = rs.getObject(i);
//             不需要像源代码一样0或false，因为这些类型不处理，也是这样的值
//              不管类型是基础类型还是包装类型，还是其它类型，数据库取出来的值是null，就不需要特别处理
            if (value==null){
                continue;
            }
//          value==null并且属性不是基本类型或者value不为null
//          value不为null,属性可能是基本类型也可能不是基本类型
            this.callSetter(bean,prop,value);
        }
        return bean;
    }

    /**
     * 此方法的主要目标就是prop.getWriterMethod.invoke(bean,value)
     * @param bean
     * @param prop
     * @param value
     * @throws SQLException
     */
    protected void callSetter(Object bean,PropertyDescriptor prop,Object value) throws SQLException {
        Method method = prop.getWriteMethod();
//      没有对应的setter方法，或者setter方法参数超过1（不规范）
        if (method==null||method.getParameterCount()!=1){
            return;
        }
//      下面主要2点逻辑
//       1:值与参数的类型是兼容的吗?
//       2:利用propertyHandler体系对值进行额外的处理
//        比如是把数据库的字符串取出来转换成枚举
        try {
            Class<?> parameterType = method.getParameterTypes()[0];
//          应用propertyHandler扩展机制
            for (PropertyHandler propertyHandler : propertyHandlers) {
                if (propertyHandler.support(parameterType,value)){
                    value=propertyHandler.apply(parameterType,value);
                    break;
                }
            }
            if (isCompatibleType(value,parameterType)){
                method.invoke(bean,value);
            }else{
                throw new SQLException("值与属性类型不兼容，值的类型是："+value.getClass()+"属性的类型是："+parameterType);
            }
        }catch (IllegalArgumentException e){
            throw new SQLException("Cannot set"+prop.getName()+":"+e.getMessage());
        }catch (IllegalAccessException e){
            throw new SQLException("Cannot set"+prop.getName()+":"+e.getMessage());
        }catch (InvocationTargetException e){
            throw new SQLException("Cannot set"+prop.getName()+":"+e.getMessage());
        }
    }

    /**
     * 目标：判断是否兼容主要确定数据能不能赋值给对应的类型
     * 主要靠Class类的isInstance方法来实现
     * 这个方法就是instanceOf关键字的反射版本
     *
     * 但是这个方法不能达到我们的要求，因为其对于value是null
     * 和目标类型是基本类型时都是返回false
     *
     * 因为null值是可以赋值给任何非基础类型（primitive）的属性
     * 而且实体类中的属性如果写成基本类型，那么你一个其对应的包装类型数据是可以给它的
     * @param value
     * @param type
     * @return
     */
    private boolean isCompatibleType(Object value,Class<?> type){
        if (type.isInstance(value)||matchesPrimitive(type,value.getClass())){
            return true;
        }
        return false;
    }

    /**
     * targetType是实体类的属性类型
     * valueType是实际的实际类型
     * @param targetType
     * @param valueType
     * @return
     */
    private boolean matchesPrimitive(Class<?> targetType,Class<?> valueType){
        if (targetType.isPrimitive()==false){
            return false;
        }
        try {
            Field field = valueType.getField("TYPE");
            Object o = field.get(valueType);
            if (valueType==o){
                return true;
            }
        } catch (NoSuchFieldException e) {
//            啥都不干，没有这样的字段产生异常表示不是包装类型

        } catch (IllegalAccessException e) {

        }
        return false;
    }

    protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,PropertyDescriptor[] props) throws SQLException {
        int clos=rsmd.getColumnCount();
        int[] result=new int[clos+1];
        Arrays.fill(result,PROPERTY_NOT_FOUND);

        for (int i = 1; i <= clos; i++) {
            String columnName = RsMetaUtils.getColumnName(rsmd, i);
            String propertyName = getMappingPropertyName(columnName);

            for (int j = 0; j < props.length; j++) {
                if (propertyName.equalsIgnoreCase(props[j].getName())){
                    result[i]=j;
                    break;
                }
            }
        }
        return result;
    }

    /**
     * 默认情况下是进行同名映射,没有考虑大小写的问题
     * 字段username 属性的名字是userName
     *
     * 但是提供了一个扩展的机制,是考Map来建立映射的规则
     * 这个map里面的key是数据库字段查询的名字,
     * value是实体类的属性名(见测试类的queryBeanPropertyOverridesHandler)
     * @param columnName
     * @return
     */
    protected String getMappingPropertyName(String columnName){
        String propertyName = propertyOverrides.get(columnName);
        return propertyName==null?columnName:propertyName;
    }

    private PropertyDescriptor[] propertyDescriptors(Class<?> clz){
        BeanInfo beanInfo=null;
        try {
            beanInfo= Introspector.getBeanInfo(clz);
        } catch (IntrospectionException e) {
            e.printStackTrace();
        }
        return beanInfo.getPropertyDescriptors();
    }

    protected <T> T createInstance(Class<?> clz) throws SQLException {
        T instance=null;
        try {
            instance= (T) clz.newInstance();
        } catch (InstantiationException e) {
            throw new SQLException("实例化出错，你的bean类有默认构造函数吗？",e);
        } catch (IllegalAccessException e) {
            throw new SQLException("实例化出错，你的bean类有默认构造函数吗？",e);
        }
        return instance;
    }

    @Deprecated
    public <T> T toBeanDeprecated(ResultSet rs,Class<?> clz) throws SQLException {
        T instance=null;
        try {
            instance= (T) clz.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        Field[] fields = clz.getDeclaredFields();

        for (Field field : fields) {
            field.setAccessible(true);
            String fieldName = field.getName();
            Object value = rs.getObject(fieldName);
            try {
                field.set(instance,value);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            field.setAccessible(false);
        }
        return instance;
    }
}
